﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace GameJam2012
{
    public class Level
    {
        public static Vector2 Gravity = new Vector2(0f, .5f);
        public ParticleSystem ps = new ParticleSystem();
        public int TileHeight = 12;
        public int TileWidth;
        public int ScreenWidth
        {
            get { return SnakeGame.TILESIZE * TileWidth; }
        }
        public int ScreenHeight
        {
            get { return SnakeGame.TILESIZE * TileHeight; }
        }

        public Texture2D whiteTile;

        public Vector2? SpawnPoint;

        protected TileType[,] map;
        protected Texture2D[,] tiles;

        /// <summary>
        /// A color to tint the entire map with
        /// </summary>
        Color tint = Color.White;

        /// <summary>
        /// A reference to the next level to the right of this one
        /// </summary>
        public Level NextLevel;
        public Level PreviousLevel;

        /// <summary>
        /// The position of the screen's left boundary
        /// </summary>
        public float LeftBound = 0f;

        List<Entity> entities = new List<Entity>();
        List<Entity> addEntities = new List<Entity>(); // entities to be added after the current update loop (to prevent updating an entity twice after a level transition)

        /// <summary>
        /// Creates a test level
        /// </summary>
        public Level()
        {
            RandomTestLevel();
            SetTiles();
        }

        /// <summary>
        /// Creates a level based on loaded level data
        /// </summary>
        /// <param name="data">The level data to copy</param>
        public Level(LevelData data)
        {
            //tint = new Color(SnakeGame.Random.Next(256), SnakeGame.Random.Next(256), SnakeGame.Random.Next(256));
            TileHeight = data.TileHeight;
            TileWidth = data.TileWidth;

            map = new TileType[TileWidth, TileHeight];

            for (int x = 0; x < TileWidth; x++)
            {
                for (int y = 0; y < TileHeight; y++)
                {
                    map[x, y] = data.Map[x, y];
                }
            }

            foreach (EntityPrototype p in data.Prototypes)
            {
                entities.Add(p.CreateEntity());
            }

            SetTiles();
        }

        protected void SetTiles()
        {
            tiles = new Texture2D[TileWidth, TileHeight];
            for (int x = 0; x < TileWidth; x++)
            {
                for (int y = 0; y < TileHeight; y++)
                {
                    if (map[x, y] == TileType.Spawn)
                    {
                        map[x, y] = TileType.Empty;
                        SpawnPoint = new Vector2(SnakeGame.TILESIZE * x + SnakeGame.TILESIZE / 2, SnakeGame.TILESIZE * y + SnakeGame.TILESIZE / 2);
                    }
                    if (map[x, y] == TileType.Wall)
                    {
                        tiles[x,y] = SnakeGame.WALLTILES[SnakeGame.Random.Next(SnakeGame.WALLTILES.Count)];
                        if (y > 0 && map[x, y - 1] != TileType.Wall) tiles[x, y] = SnakeGame.SURFACETILES[SnakeGame.Random.Next(SnakeGame.SURFACETILES.Count)];
                    }
                    if (map[x, y] == TileType.Platform)
                    {
                        tiles[x, y] = SnakeGame.WALLTILE;
                        map[x, y] = TileType.Wall;
                    }
                }
            }
        }

        /// <summary>
        /// Generates a random test level
        /// </summary>
        protected void RandomTestLevel()
        {
            tint = new Color(SnakeGame.Random.Next(256), SnakeGame.Random.Next(256), SnakeGame.Random.Next(256));
            TileWidth = SnakeGame.Random.Next(5, 10);
            map = new TileType[TileWidth, TileHeight];

            for (int i = 0; i < 15; i++)
            {
                //map[SnakeGame.Random.Next(TileWidth), SnakeGame.Random.Next(TileHeight)] = TileType.Wall;
            }
            for (int x = 0; x < TileWidth; x++)
                map[x, TileHeight - 1] = TileType.Wall;

            //SpawnPoint = new Vector2(128, 256);
        }

        public void AddEntity(Entity e)
        {
            AddEntity(e, true);
        }

        /// <summary>
        /// Adds an entity to the map
        /// </summary>
        /// <param name="e">The entity to add</param>
        /// <param name="allowUpdateNow">Allows the entity to be updated immediately after being added</param>
        public void AddEntity(Entity e, bool allowUpdateNow)
        {
            if (allowUpdateNow)
                entities.Add(e);
            else
                addEntities.Add(e);
        }

        /// <summary>
        /// Checks for a collision with the world
        /// </summary>
        /// <param name="bound">A bounding box to check</param>
        /// <param name="screenBounds">Whether or not to check for screen boundaries</param>
        /// <param name="checkAdjacentLevels">Whether to check for collisions in adjacent levels when on the edge</param>
        /// <returns></returns>
        public Collision CheckWorldCollision(FloatRectangle bound, bool screenBounds, bool checkAdjacentLevels)
        {
            int x1 = (int)(bound.X / SnakeGame.TILESIZE)-1;
            int y1 = (int)(bound.Y / SnakeGame.TILESIZE)-1;
            int x2 = (int)(bound.Right / SnakeGame.TILESIZE)+1;
            int y2 = (int)(bound.Bottom / SnakeGame.TILESIZE)+1;
            //if (bound.Top < 0) return new Collision(new FloatRectangle(0, -64, ScreenWidth, 64));
            if (bound.Bottom >= ScreenHeight) return new Collision(new FloatRectangle(0, ScreenHeight-1, ScreenWidth, 64));
            
            if (screenBounds)
            {
                if (PreviousLevel == null && bound.Left < LeftBound) return new Collision(new FloatRectangle(-64, 0, LeftBound+64, ScreenHeight));
                if (NextLevel == null && bound.Right > ScreenWidth) return new Collision(new FloatRectangle(ScreenWidth, 0, 64, ScreenHeight));
            }
            for (int y = y1; y <= y2; y++)
            {
                for (int x = x1; x < x2; x++)
                {
                    if (x >= 0 && y >= 0 && x < TileWidth && y < TileHeight)
                    {
                        if (map[x, y] == TileType.Wall)
                        {
                            FloatRectangle worldBound = new FloatRectangle(x * SnakeGame.TILESIZE, y * SnakeGame.TILESIZE, SnakeGame.TILESIZE, SnakeGame.TILESIZE);
                            if (worldBound.Intersects(bound))
                                return new Collision(worldBound);
                        }
                    }
                }
            }

            if (checkAdjacentLevels)
            {
                if (bound.Left < 0 && PreviousLevel != null)
                {
                    Collision c = PreviousLevel.CheckWorldCollision(new FloatRectangle(bound.X + PreviousLevel.ScreenWidth, bound.Y, bound.Width, bound.Height), screenBounds, false);
                    if (c != null) 
                    {
                        c.CollisionRectangle.X -= PreviousLevel.ScreenWidth; //convert to local coordinates
                        return c; 
                    }
                } else if (bound.Right >= ScreenWidth && NextLevel != null)
                {
                    Collision c = NextLevel.CheckWorldCollision(new FloatRectangle(bound.X - ScreenWidth, bound.Y, bound.Width, bound.Height), screenBounds, false);

                    if (c != null)
                    {
                        c.CollisionRectangle.X += ScreenWidth;
                        return c;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Called once per frame to handle game logic
        /// </summary>
        /// <param name="gameTime">The current game time structure</param>
        public void Update(GameTime gameTime)
        {
            
            foreach (Entity e in entities)
            {
                e.Update(gameTime, this);

            }

            foreach (Entity e1 in entities)
            {
                foreach (Entity e2 in entities)
                {
                    if (e1 != e2 && e1.CheckCollisionWith(e2.GetBoundingBox()))
                    {
                        e1.CollideWith(e2);
                    }
                }
            }

            // cleanup the entities list
            for (int i = 0; i < entities.Count; i++)
            {
                Entity e = entities[i];

                // check if the entity has moved off this level and update accordingly
                if (e.Position.X >= ScreenWidth)
                {
                    e.Position.X -= ScreenWidth;

                    if (NextLevel != null)
                    {
                        entities.RemoveAt(i--);
                        NextLevel.AddEntity(e, false);
                    }
                    else
                    {
                        e.Kill();
                    }
                }
                else if (e.Position.X < 0)
                {
                    if (PreviousLevel != null)
                    {
                        entities.RemoveAt(i--);
                        e.Position.X += PreviousLevel.ScreenWidth;
                        PreviousLevel.AddEntity(e);
                    }
                    else
                    {
                        e.Kill();
                    }
                }
                else if (e.Dead)
                {
                    entities.RemoveAt(i);
                }
            }

            entities.AddRange(addEntities);
            addEntities = new List<Entity>();
            ps.Update();
        }

        /// <summary>
        /// Called once per frame to draw everything
        /// </summary>
        /// <param name="spriteBatch">The SpriteBatch object to use</param>
        /// <param name="xOffset">The screen offset of the level and everything in it</param>
        public void Draw(SpriteBatch spriteBatch, float xOffset, float yOffset)
        {
            //spriteBatch.Draw(SnakeGame.BACKTILE, new Rectangle((int)xOffset, (int)yOffset, ScreenWidth, ScreenHeight), new Rectangle(0, 0, ScreenWidth, ScreenHeight), tint, 0f, Vector2.Zero, SpriteEffects.None, 0f);
            for (int x = 0; x < TileWidth; x++)
            {
                for (int y = 0; y < TileHeight; y++)
                {
                    if(tiles[x,y] != null)
                        spriteBatch.Draw(tiles[x,y], new Rectangle(x * SnakeGame.TILESIZE + (int)xOffset, y * SnakeGame.TILESIZE + (int)yOffset, SnakeGame.TILESIZE, SnakeGame.TILESIZE), null, tint, 0f, Vector2.Zero, SpriteEffects.None, .5f );
                }
            }

            foreach (Entity e in entities)
            {
                e.Draw(spriteBatch, xOffset, yOffset, tint);

            }
            Vector2 offset = new Vector2(xOffset,yOffset);
            //ps.Draw(spriteBatch, offset*-1);
            ps.Draw(spriteBatch, offset);
        }
    }

    public class Collision
    {
        public Collision(FloatRectangle bound)
        {
            CollisionRectangle = bound;
        }

        public FloatRectangle CollisionRectangle;
    }

    /// <summary>
    /// This class holds level data loaded from a file
    /// </summary>
    public class LevelData
    {
        public int TileWidth;
        public int TileHeight = 12;

        public TileType[,] Map;

        public List<EntityPrototype> Prototypes;

        public LevelData()
        {

        }

        /// <summary>
        /// Loads a given level
        /// </summary>
        /// <param name="path">The path of the level to be loaded</param>
        /// <returns>Whether or not the level loaded successfully</returns>
        public bool LoadLevel(string path)
        {
            try
            {
                using (StreamReader r = new StreamReader(path))
                {
                    String line = r.ReadLine();
                    TileWidth = Convert.ToInt32(line);
                    Map = new TileType[TileWidth, TileHeight];
                    Prototypes = new List<EntityPrototype>();
                    for (int y = 0; y < TileHeight; y++)
                    {
                        line = r.ReadLine();
                        for (int x = 0; x < TileWidth; x++)
                        {
                            switch (line[x])
                            {
                                case '#':
                                    Map[x, y] = TileType.Wall;
                                    break;
                                case '=':
                                    Map[x, y] = TileType.Platform;
                                    break;
                                case '$':
                                    Prototypes.Add(new FoodPrototype(x, y));
                                    break;
                                case 'X':
                                    Prototypes.Add(new DeathEntityPrototype(x, y));
                                    break;
                                case '@':
                                    Map[x, y] = TileType.Spawn;
                                    break;
                                case '^':
                                    Prototypes.Add(new SpikePrototype(x, y));
                                    break;
                                case '*':
                                    Prototypes.Add(new SawBladePrototype(x, y));
                                    break;
                                default:
                                    Map[x, y] = TileType.Empty;
                                    break;
                            }
                        }
                    }
                    return true;
                }
            }
            catch
            {
                Console.WriteLine("Failed to load " + path);
                return false;
            }
        }
    }

    public class EntityPrototype
    {
        public virtual Entity CreateEntity()
        {
            return null;
        }
    }

    class FoodPrototype : EntityPrototype
    {
        int x;
        int y;

        public FoodPrototype(int x, int y) : base () {
            this.x = x;
            this.y = y;
        }

        public override Entity CreateEntity()
        {
            return new Food(x, y);
        }
    }

    class DeathEntityPrototype : EntityPrototype
    {
        int x;
        int y;

        public DeathEntityPrototype(int x, int y) : base () {
            this.x = x;
            this.y = y;
        }

        public override Entity CreateEntity()
        {
            return new DeathEntity(x, y);
        }
    }

    class SpikePrototype : EntityPrototype
    {
        int x;
        int y;

        public SpikePrototype(int x, int y)
            : base()
        {
            this.x = x;
            this.y = y;
        }

        public override Entity CreateEntity()
        {
            return new Spike(x, y);
        }
    }

    class SawBladePrototype : EntityPrototype
    {
        int x;
        int y;

        public SawBladePrototype(int x, int y)
            : base()
        {
            this.x = x;
            this.y = y;
        }

        public override Entity CreateEntity()
        {
            return new SawBlade(x, y);
        }
    }

    public enum TileType { Empty, Wall, Spawn, Platform };
}
